/*
 * IC Project - a prototype to test a hypothesis of collective intelligence
 * Copyright (C) 2012 Paolo Conte - paolo@concavi.net
 * 
 * This program is free software: you can redistribute it and/or modify
 * it under the terms of the GNU Affero General Public License as
 * published by the Free Software Foundation, either version 3 of the
 * License, or (at your option) any later version.
 * 
 * This program is distributed in the hope that it will be useful,
 * but WITHOUT ANY WARRANTY; without even the implied warranty of
 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.
 * See the GNU Affero General Public License for more details.
 * 
 * You should have received a copy of the GNU Affero General Public License
 * along with this program.  If not, see <http://www.gnu.org/licenses/>.
 */

/*
 * This code is derived from the projects ojoverse (2008) and ModelDB (2003)
 * under the license GPL2. See <http://www.gnu.org/licenses/>.
 */

package org.ix.data.manager;

import java.lang.reflect.Field;
import java.math.BigDecimal;
import java.sql.Clob;
import java.sql.Connection;
import java.sql.PreparedStatement;
import java.sql.ResultSet;
import java.sql.SQLException;
import java.sql.Statement;
import java.sql.Date;
import java.sql.Timestamp;
import java.util.Enumeration;
import javax.servlet.http.HttpServletRequest;

/**
 * Static methods container, used to create Java and SQL code, to mapping and loading objects from HttpRequests, to setting PreparedStatements and Resultsets from objects  
 */
public class POJOManager {

    public static final int SQL_INSERT = 2;
    public static final int SQL_UPDATE = 3;
    public static final int SQL_DELETE = 4;

    /**
     * Set object fields using same-name request parameters
     * @param request HttpServletRequest
     * @param data Empty object to load
     * @return Loaded object
     * @throws IllegalArgumentException
     * @throws IllegalAccessException
     */
    public static Object loadObject(HttpServletRequest request, Object data) {
        Enumeration enumRequest = request.getParameterNames();
        Field[] dataFields = data.getClass().getFields();

        int errMaxSize = 10;
        String[][] errors = new String[errMaxSize][2];
        int errCount = 0;
        while (enumRequest.hasMoreElements()) {
            String name = (String) enumRequest.nextElement();
            String value = request.getParameter(name);
            if (value == null || value.equals("")) {
                continue;
            }
            for (int idf = 0; idf < dataFields.length; idf++) {
                Field field = dataFields[idf];
                String fieldName = field.getName();
                if (fieldName.equals(name)) {
                    try {
                        field.setAccessible(true);

                        Class type = field.getType();
                        if (type.isAssignableFrom(String.class)) {
                            field.set(data, value);
                        } else if (type.isAssignableFrom(Integer.class)) {
                            field.set(data, Integer.valueOf(value));
                        } else if (type.isAssignableFrom(Long.class)) {
                            field.set(data, Long.valueOf(value));
                        } else if (type.isAssignableFrom(Double.class)) {
                            field.setDouble(data, Double.valueOf(value));
                        } else if (type.isAssignableFrom(BigDecimal.class)) {
                            field.set(data, BigDecimal.valueOf(Double.parseDouble(value)));
                        } else if (type.isAssignableFrom(Date.class)) {
                            field.set(data, Date.valueOf(value));
                        } else if (type.isAssignableFrom(Timestamp.class)) {
                            field.set(data, new Timestamp(Date.valueOf(value).getTime()));
                        } else if (type.isAssignableFrom(Clob.class)) {
                            field.set(data, (Clob) (Object) value);
                        }
                    } catch (Exception exc) {
                        if (errCount < errMaxSize) {
                            errors[errCount][0] = name;
                            errors[errCount][1] = value.toString();
                        }
                        errCount++;
                    }
                }
            }
        }
        if (errCount > 0) {
            request.setAttribute("loaderrors", errors);
        }
        return data;
    }

    /**
     * Create the SQL WHERE clause from a POJO object
     * if field value is not null add a SQL block: "FIELDNAME = ?"
     * @param data
     * @return SQL string
     */
    public static String createWhereClause(Object data) {
        if (data == null) {
            return "";
        }

        Field[] fields = data.getClass().getFields();

        if (fields == null) {
            return "";
        }

        int fieldLen = fields.length;

        StringBuffer where = new StringBuffer(1000);
        // loop on data fields
        for (int idf = 0; idf < fieldLen; idf++) {
            Field field = fields[idf];
            field.setAccessible(true);
            String name = field.getName();

            Object value = null;
            try {
                value = field.get(data);
            } catch (Exception exc) {
                continue;
            }
            if (value == null) {
                continue;
            }
            StringBuffer whereClause = new StringBuffer(50);
            whereClause.append(name.toUpperCase());
            whereClause.append(" = ?");

            if (where.length() > 0) {
                where.append(" AND ");
            }
            where.append(whereClause);
        }

        return where.toString();
    }

    public static PreparedStatement getPreparedStatement(Connection cnn, String query) throws SQLException {
        return cnn.prepareStatement(query, ResultSet.TYPE_FORWARD_ONLY, ResultSet.CONCUR_READ_ONLY);
    }

    /**
     * Used with previous @see createWhereClause
     * Load a PreparedStatement using a DataPOJO:
     * for each not null field set the PreparedStatement field value
     * @param data
     * @return loaded PreparedStatement
     */
    public static PreparedStatement prepareStatement(PreparedStatement prstm, Object data) throws SQLException {
        if (data == null) {
            return prstm;
        }
        Field[] fields = data.getClass().getFields();

        int fieldLen = fields.length;
        int pos = 1;
        for (int idf = 0; idf < fieldLen; idf++) {
            Field field = fields[idf];
            field.setAccessible(true);

            Object value = null;
            try {
                value = field.get(data);
            } catch (Exception exc) {
                continue;
            }
            if (value == null) {
                continue;
            }
            Class type = field.getType();
            String valueAsString = value.toString();
            if (type.isAssignableFrom(String.class)) {
                prstm.setString(pos++, valueAsString);
            } else if (type.isAssignableFrom(Integer.class)) {
                prstm.setInt(pos++, Integer.parseInt(valueAsString));
            } else if (type.isAssignableFrom(Long.class)) {
                prstm.setLong(pos++, Long.parseLong(valueAsString));
            } else if (type.isAssignableFrom(Double.class)) {
                prstm.setDouble(pos++, Double.parseDouble(valueAsString));
            } else if (type.isAssignableFrom(BigDecimal.class)) {
                prstm.setBigDecimal(pos++, BigDecimal.valueOf(Double.parseDouble(valueAsString)));
            } else if (type.isAssignableFrom(Date.class)) {
                prstm.setDate(pos++, Date.valueOf(valueAsString));
            } else if (type.isAssignableFrom(Timestamp.class)) {
                prstm.setTimestamp(pos++, (Timestamp) value);
            } else if (type.isAssignableFrom(Clob.class)) {
                prstm.setClob(pos++, (Clob) value);
            }
        }

        return prstm;
    }

    public static Object loadObject(ResultSet rset, Object data) throws SQLException, IllegalArgumentException, IllegalAccessException {
        Field[] fields = data.getClass().getFields();

        int fieldLen = fields.length;
        for (int idf = 0; idf < fieldLen; idf++) {
            Field field = fields[idf];
            String fieldName = field.getName().toUpperCase();
            field.setAccessible(true);

            Class type = field.getType();
            if (type.isAssignableFrom(String.class)) {
                String value = rset.getString(fieldName);
                field.set(data, value);
            } else if (type.isAssignableFrom(Integer.class)) {
                field.set(data, rset.getInt(fieldName));
            } else if (type.isAssignableFrom(Long.class)) {
                field.set(data, rset.getLong(fieldName));
            } else if (type.isAssignableFrom(Double.class)) {
                field.setDouble(data, rset.getDouble(fieldName));
            } else if (type.isAssignableFrom(BigDecimal.class)) {
                field.set(data, rset.getBigDecimal(fieldName));
            } else if (type.isAssignableFrom(Date.class)) {
                field.set(data, rset.getDate(fieldName));
            } else if (type.isAssignableFrom(Timestamp.class)) {
                field.set(data, rset.getTimestamp(fieldName));
            } else if (type.isAssignableFrom(Clob.class)) {
                field.set(data, rset.getClob(fieldName));
            }
        }

        return data;
    }

    public static String createQuery(String tableName, Object data, int type) {
        if (data == null) {
            return "";
        }
        
        Field[] fields = data.getClass().getFields();
        StringBuffer sb = new StringBuffer(900);
        int fieldLen = fields.length;

        switch (type) {
            case SQL_INSERT:
                sb.append("INSERT INTO ").append(tableName).append("(");
                break;
            case SQL_UPDATE:
                sb.append("UPDATE ").append(tableName);
                break;
            case SQL_DELETE:
                sb.append("DELETE FROM ").append(tableName);
                break;
        }


        int count = 0;
        for (int idf = 0; idf < fieldLen; idf++) {
            Field field = fields[idf];
            String fieldName = field.getName().toUpperCase();

            field.setAccessible(true);
            Object value = null;
            try {
                value = field.get(data);
            } catch (Exception exc) {
                continue;
            }
            if (value == null) {
                continue;
            }

            switch (type) {
                case SQL_INSERT:
                    if (count > 0) {
                        sb.append(", ");
                    }
                    sb.append(fieldName);
                    break;
                case SQL_UPDATE:
                    if (count == 0) {
                        sb.append(" SET ");
                    } else {
                        sb.append(", ");
                    }
                    sb.append(fieldName).append("=?");
                    break;
            }

            count++;
        }

        if (type == SQL_INSERT) {
            sb.append(") VALUES (");
            for (int idx = 0; idx < count; idx++) {
                if (idx > 0) {
                    sb.append(", ");
                }
                sb.append("?");
            }
            sb.append(")");
        }

        return sb.toString();
    }

    public static boolean isComplete(Object data) {
        if (data == null) {
            return false;
        }
        Field[] fields = data.getClass().getFields();

        int fieldLen = fields.length;
        for (int idf = 0; idf < fieldLen; idf++) {
            Field field = fields[idf];
            if (field.getName().toUpperCase().indexOf("PKID")>0) {
                continue;
            }
            field.setAccessible(true);

            Object value = null;
            try {
                value = field.get(data);
            } catch (Exception exc) {
                return false;
            }
            if (value == null) {
                return false;
            }
        }

        return true;
    }

    public static String objectToString(Object data) {
        Field[] fields = data.getClass().getFields();
        String className = data.getClass().getName().substring(data.getClass().getName().indexOf("$") + 1);
        StringBuffer sb = new StringBuffer();
        sb.append("[").append(className).append("]");
        for (int idf = 0; idf < fields.length; idf++) {
            Field field = fields[idf];
            field.setAccessible(true);
            if (idf > 0) {
                sb.append(", ");
            }
            sb.append(field.getName()).append(":");
            try {
                sb.append(field.get(data));
            } catch (Exception exc) {
                sb.append("{error}");
            }
        }
        return sb.toString();
    }

    public static String objectToJSON(Object data) {
        Field[] fields = data.getClass().getFields();
        StringBuffer sb = new StringBuffer();
        sb.append("{");
        for (int idf = 0; idf < fields.length; idf++) {
            Field field = fields[idf];
            field.setAccessible(true);
            if (idf > 0) {
                sb.append(", ");
            }
            sb.append('\"');
            sb.append(escape(field.getName()));
            sb.append('\"');
            sb.append(':');
            // TODO: add datatype management
            try {
                sb.append(field.get(data));
            } catch (Exception exc) {
                sb.append("---error---");
            }
        }
        sb.append("}");
        return sb.toString();
    }

    public static String escape(String value) {
        StringBuffer sbEscape = new StringBuffer();
        for (int i = 0; i < value.length(); i++) {
            char aChar = value.charAt(i);
            switch (aChar) {
                case '/':
                    sbEscape.append("\\/");
                    break;
                case '\n':
                    sbEscape.append("\\n");
                    break;
                case '\r':
                    sbEscape.append("\\r");
                    break;
                case '\t':
                    sbEscape.append("\\t");
                    break;
                case '"':
                    sbEscape.append("\\\"");
                    break;
                case '\\':
                    sbEscape.append("\\\\");
                    break;
                case '\b':
                    sbEscape.append("\\b");
                    break;
                case '\f':
                    sbEscape.append("\\f");
                    break;
                default:
                    sbEscape.append(aChar);
            }
        }
        return sbEscape.toString();
    }

    /**
     * Close a ResultSet and/or a (Prepared)Statement
     * @param resultSet The ResultSet to close, or null
     * @param statement The Statement to close, or null
     */
    public static void close(ResultSet resultSet) {
        close(resultSet, null);
    }

    public static void close(Statement statement) {
        close(null, statement);
    }

    public static void close(ResultSet resultSet, Statement statement) {
        try {
            if (resultSet != null) {
                resultSet.close();
            }
            if (statement != null) {
                statement.close();
            }
        } catch (SQLException sqle) {
        }
    }
}
